package eu.goldenak.ircbot.module;

import eu.goldenak.ircbot.*;
import eu.goldenak.ircbot.BotGroup.DistributeMessage;
import eu.goldenak.ircbot.module.CoD4EchoHandler.CoD4Query.Player;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Array;
import java.net.*;
import java.util.*;

/**
 * Module for the bot to echo the contents of a log from a CoD4 server into some
 * nicely formatted and understandable messages to an IRC channel.
 */
public class CoD4EchoHandler extends ModuleBase {
	/**
	 * The config loaded from echohandler.cfg.
	 */
	private HashMap<String, String> config = null;
	
	/**
	 * This object handles the logfile of CoD4.
	 */
	private LogHandler log;
	
	/**
	 * Formatted outgoing messages which are going to be distributed.
	 */
	private Vector<DistributeMessage> fMsg = new Vector<DistributeMessage>();
	
	/**
	 * Stats of all players in the server.
	 */
	private PlayerStats[] players = new PlayerStats[64];
	
	/**
	 * A playername to playerID map.
	 */
	private HashMap<String, Integer> playerID = new HashMap<String, Integer>();
	
	/**
	 * The channel where the messages should go to.
	 */
	private String echoChannel;
	
	/**
	 * The channels from where commands are accepted.
	 */
	private String[] cmdChannel;
	
	/**
	 * An object which handles rcon connections.
	 */
	private Rcon rcon;
	
	/**
	 * Since we only loop once per second, we need to save the time of
	 * the last loop somewhere. Which is here.
	 */
	private long lastLoop = 0;
	
	/**
	 * Constructor. Loads up the configfile and sets up the Rcon and LogHandler
	 * objects.
	 */
	public CoD4EchoHandler() throws Exception {
		try {
			config = ConfigManager.getModule ("CoD4EchoHandler");
		} catch (Exception ex) {
			System.out.println("Error: " + ex);
			ex.printStackTrace();
			System.exit(1);
		}
		
		// Setup rcon object.
		try {
			rcon = new Rcon (
				config.get ("rconhost"),
				Integer.parseInt (config.get ("rconport")),
				config.get ("rconpass"),
				Integer.parseInt (config.get ("rcontimeout"))
			);
		} catch (UnknownHostException ex) {
			System.out.println("Error with rcon hostname: " + ex);
			ex.printStackTrace();
		}
		
		echoChannel = config.get("echochannel");
		cmdChannel = config.get("cmdchannels").toLowerCase().split(",");
		Arrays.sort(cmdChannel);
		
		if (config.get("mode").equals("ftp")) {
			// TODO make LogHandler a subclass.
			log = new LogHandler (config.get("ftphost"), Integer.parseInt(config.get("ftpport")), config.get("logfilepath"), config.get("logfilename"));
			log.setUsername(config.get("ftpusername"));
			log.setPassword(config.get("ftppassword"));
			if (config.containsKey("ftpmode")) {
				log.setFtpMode(Integer.parseInt(config.get("ftpmode")));
			}
			log.connect();
		} else if (config.get("mode").equals("file")) {
			log = new LogHandler (config.get("logfilepath"), config.get("logfilename"));
		} else {
			throw new Exception("Unknown mode given for module EchoHandler.");
		}
	}
	
	/**
	 * The place where everything happens. Messages are retrieved, parsed, 
	 * formatted and sent to IRC.
	 */
	public void onTick () {
		if (lastLoop < (System.currentTimeMillis() - 1000)) {
			// Only once per second
			lastLoop = System.currentTimeMillis();
		} else {
			return;
		}
		
		// Process rcon stuff
		try {
			rcon.loop();
			if (rcon.hasResult()) {
				Rcon.RconMsg rconRes = rcon.getResult();
				
				String[] res = rconRes.res.split("\n");
				String[] cmd = rconRes.cmd.split("\\s+");
				
				// TODO Test for other responses.
				if (res[0].startsWith("print")) {
					// Rcon command was correctly received.
					if (cmd[0].equals("say")) {
						if (cmd[1].substring(0, 1).equals("[") && cmd[1].substring(cmd[1].length() - 1, cmd[1].length()).equals("]")) {
							String msg = "";
							for (int x = 2; x < cmd.length; x++) msg += cmd[x] + " ";
							fMsg.add(new DistributeMessage(echoChannel, formatMsg("\u000314[--] \u000307%s\u000F: %s", cmd[1].substring(1, cmd[1].length() - 1), msg)));
						} else {
							String msg = "";
							for (int x = 1; x < cmd.length; x++) msg += cmd[x] + " ";
							fMsg.add(new DistributeMessage(echoChannel, formatMsg("\u000314[--] \u000307console\u000F: %s", msg)));
						}
					} 
				} else {
					// FIXME Outputs too much crap, sometimes even floods
					for (int i = 0; i < res.length; i++) {
						fMsg.add(new DistributeMessage(echoChannel, res[i]));
					}
				}
			}
		} catch (Exception ex) {
			fMsg.add(new DistributeMessage(echoChannel, "Rcon command could not be sent: " + ex.getMessage()));
			ex.printStackTrace();
		}
		
		String[] newMsgs = log.readNewLines();
		if (newMsgs == null) {
			// Nothing new.
			return;
		}
		
		for (int i = 0; i < newMsgs.length; i++) {
			String[] tmp = newMsgs[i].trim().split(" ", 2);
			newMsgs[i] = tmp[1];
			String[] line = newMsgs[i].split(";");
			//System.out.println("MODULE: Processing line: " + newMsgs[i]);
			
			if (line[0].equals("D")) {
				/**
				 * Damage
				 * 
				 * D;44313adf9e4d06321e56677cce28f913;2;allies;[GAS]no10;44313adf9e4d06321e56677cce28f913;2;allies;[GAS]no10;;5;MOD_MELEE;none
				 */
				int idKilled, idKiller;
				idKilled = Integer.parseInt(line[2]);
				idKiller = Integer.parseInt(line[6]);
				
				updatePlayer(idKilled, line[4], line[1]);
				if (idKilled != idKiller) {
					// Not damaging him/herself.
					updatePlayer(idKiller, line[8], line[5]);
				}
				 
				// Get the teams of the players
				if (line[3].equals("axis")) {
					players[idKilled].team = false;
				} else if (line[3].equals("allies")) {
					players[idKilled].team = true;
				}
				
				// By seperating the blocks for both players, teamkills are supported too.
				if (line[7].equals("axis")) {
					players[idKiller].team = false;
				} else if (line[7].equals("allies")) {
					players[idKiller].team = true;
				}
				
				if (line[11].equals("MOD_GRENADE_SPLASH")) {
					fMsg.add(new DistributeMessage(echoChannel, formatMsg("{NAME} stunned {NAME}", Integer.parseInt(line[6]), "\u000305" + line[8], Integer.parseInt(line[2]), "\u000305" + line[4])));
				} else if (line[11].equals("MOD_FALLING")) {
					fMsg.add(new DistributeMessage(echoChannel, formatMsg("{NAME} fell with %s damage", Integer.parseInt(line[2]), "\u000305" + line[4], line[10])));
				} else {
					//fMsg.add(new DistributeMessage(echoChannel, newMsgs[i]));
				}
			} else if (line[0].equals("K")) {
				/**
				 * Kill
				 * 0,1      ,2 ,,4         ,5      ,6 ,,8         ,9     ,10    ,11  ,12      
				 * K,keyhash,id,,nameKilled,keyhash,id,,nameKiller,weapon,damage,type,bodypart
				 * 33:27 K;16aec14c796b732afed1ad6b7798b803;0;;hi;6cec06da5997331e8f3c6463b264fe69;1;;Commander_S;m40a3_mp;147;MOD_HEAD_SHOT;head
				 * 33:27 K;6cec06da5997331e8f3c6463b264fe69;1;;Commander_S;9f3b65a3005d6c939639d5ec4aefb1fb;3;;*LVP*slein;mp44_mp;139;MOD_MELEE;torso_lower
				 * 33:35 K;6cec06da5997331e8f3c6463b264fe69;1;;Commander_S;9f3b65a3005d6c939639d5ec4aefb1fb;3;;*LVP*slein;mp44_mp;40;MOD_RIFLE_BULLET;right_leg_upper
				 */
				int idKilled, idKiller;
				idKilled = Integer.parseInt(line[2]);
				idKiller = Integer.parseInt(line[6]);
				
				updatePlayer(idKilled, line[4], line[1]);
				if (idKilled != idKiller) {
					updatePlayer(idKiller, line[8], line[5]);
				}
				
				if (players[idKilled].currentKillstreak >= 3) {
					fMsg.add(new DistributeMessage(echoChannel, formatMsg("{NAME}\'s killing spree has come to an end at %d kills", idKilled, "\u000306" + line[4], players[idKilled].currentKillstreak)));
				}
				
				players[idKilled].currentKillstreak = 0;
				players[idKilled].currentDeathstreak++;
				players[idKilled].deaths.add(new PlayerKill(idKiller, line[9], line[12], line[11]));
				// Update deathstreak if it's bigger.
				if (players[idKilled].biggestDeathstreak < players[idKilled].currentDeathstreak) {
					players[idKilled].biggestDeathstreak = players[idKilled].currentDeathstreak;
				}
				
				if (idKiller != -1) {
					players[idKiller].currentDeathstreak = 0;
					players[idKiller].currentKillstreak++;
					players[idKiller].kills.add(new PlayerKill(idKilled, line[9], line[12], line[11]));
					// Update killstreak if it's bigger.
					if (players[idKiller].biggestKillstreak < players[idKiller].currentKillstreak) {
						players[idKiller].biggestKillstreak = players[idKiller].currentKillstreak;
					}
					
					// Some extra info messages.
					if (players[idKiller].currentKillstreak == 3) {
						fMsg.add(new DistributeMessage(echoChannel, formatMsg("{NAME} has a 3 kill streak, plus UAV", idKiller, "\u000306" + line[8])));
					} else if (players[idKiller].currentKillstreak == 5) {
						fMsg.add(new DistributeMessage(echoChannel, formatMsg("{NAME} has a 5 kill streak, plus an airstrike", idKiller, "\u000306" + line[8])));
					} else if (players[idKiller].currentKillstreak == 7) {
						fMsg.add(new DistributeMessage(echoChannel, formatMsg("{NAME} has a 7 kill streak, plus a heli", idKiller, "\u000306" + line[8])));
					} else if (players[idKiller].currentKillstreak % 5 == 0) {
						fMsg.add(new DistributeMessage(echoChannel, formatMsg("{NAME} has a %d kill streak", idKiller, "\u000306" + line[8], players[idKiller].currentKillstreak)));
					}
				}
				
				if (line[8].equals(line[4])) {
					fMsg.add(new DistributeMessage(echoChannel, formatMsg("{NAME} commited suicide", idKilled, "\u000304" + line[4])));
				} else if (line[8].length() == 0) {
					fMsg.add(new DistributeMessage(echoChannel, formatMsg("{NAME} fell to his/her death", idKilled, "\u000304" + line[4])));
				} else {
					String killInfo = "";
					switch (line [11].charAt (4))
					{
						case 'M':
							killInfo += "knife - " + parseBodyPart (line [12]);
							break;
						
						case 'H':
							killInfo = parseWeapon (line [9]);
							killInfo += " - headshot";
							break;
						
						default:
						case 'R':
							killInfo = parseWeapon (line [9]);
							if (!line [12].equals ("none")) {
								killInfo += " - " + parseBodyPart (line [12]);
							}
							break;
					}
					
					fMsg.add(new DistributeMessage(echoChannel, formatMsg("{NAME} \u000301killed {NAME}\u000F (%s)", idKiller, "\u000304" + line[8], idKilled, "\u000304" + line[4], killInfo)));
				}
			} else if (line[0].equals("J")) {
				/**
				 * J;2ba5d1ba121e11f6bff2f73bda8e6d2c;8;*LVP*Sander
				 */
				int id = Integer.parseInt(line[2]);
				updatePlayer(id, line[3], line[1]);
				
				fMsg.add(new DistributeMessage(echoChannel, formatMsg("{NAME} joined", id, "\u000303" + line[3])));
				// Send to admins.
				fMsg.add(new DistributeMessage("%" + echoChannel, formatMsg("\u000305Keyhash of {NAME}: %s", id, "\u000305" + line[3], line[1])));
			} else if (line[0].equals("Q")) {
				/**
				 * Q;2ba5d1ba121e11f6bff2f73bda8e6d2c;8;*LVP*Sander
				 */
				int nId = Integer.parseInt(line[2]);
				
				if (players [nId] == null) {
					// No join, only a quit. This fella timed out while connecting. Show that.
					
					fMsg.add(new DistributeMessage(echoChannel, formatMsg("{NAME} timed out while connecting", nId, "\u000303" + line[3])));
				}
				else {
					// Quickly update nickchange, just because it's neat.
					updatePlayer(nId, line[3], line[1]);
					
					// Fine, remove it.
					players[nId] = null;
					playerID.remove(line[3]);
					
					fMsg.add(new DistributeMessage(echoChannel, formatMsg("{NAME} quit", nId, "\u000303" + line[3])));
				}
			} else if (line[0].equals("Weapon")) {
				/**
				 * Weapon;;8;bot8;m4_reflex_mp
				 */
				int id = Integer.parseInt(line[2]);
				updatePlayer(id, line[3], line[1]);
			} else if (line[0].equals("say")) {
				/**
				 * say;2ba5d1ba121e11f6bff2f73bda8e6d2c;8;*LVP*Sander;ded
				 */
				int id = Integer.parseInt(line[2]);
				updatePlayer(id, line[3], line[1]);
				
				String chatMsg = "";
				for (int p = 4; p < line.length; p++) chatMsg += line[p] + ";"; 
				chatMsg = chatMsg.substring(0, chatMsg.length() - 1);
				
				if (chatMsg.substring(0, 1).equals("\u0014") || chatMsg.substring(0,1).equals("\u0015")) {
					chatMsg = chatMsg.substring(1);
				}
				if (chatMsg.length() > 12 && chatMsg.substring(0, 12).equals("QUICKMESSAGE")) {
					String tmp2 = chatMsg.substring(13).toLowerCase().replace("_", " ");
					chatMsg = "\u000311" + tmp2.substring(0, 1).toUpperCase() + tmp2.substring(1) + "!";
				}
				
				fMsg.add(new DistributeMessage(echoChannel, formatMsg("{NAME}\u000F: %s", id, "\u000307" + line[3], parseColor(chatMsg))));
				
				// TODO ingamecommands
			} else if (line[0].equals("sayteam")) {
				int id = Integer.parseInt(line[2]);
				updatePlayer(id, line[3], line[1]);
				
				String chatMsg = "";
				for (int p = 4; p < line.length; p++) chatMsg += line[p] + ";";
				chatMsg = chatMsg.substring(0, chatMsg.length() - 1);
				
				if (chatMsg.substring(0, 1).equals("\u0014") || chatMsg.substring(0,1).equals("\u0015")) {
					chatMsg = chatMsg.substring(1);
				}
				if (chatMsg.length() > 12 && chatMsg.substring(0, 12).equals("QUICKMESSAGE")) {
					String tmp2 = chatMsg.substring(13).toLowerCase().replace("_", " ");
					chatMsg = "\u000311" + tmp2.substring(0, 1).toUpperCase() + tmp2.substring(1) + "!";
				}
				
				fMsg.add(new DistributeMessage("%" + echoChannel, formatMsg("\u0002\u000305Team\u0002 {NAME}\u000F: %s", id, "\u000307" + line[3], parseColor(chatMsg))));
			} else if (line[0].equals("------------------------------------------------------------")) {
				//fMsg.add(new DistributeMessage(echoChannel, "---"));
			} else {
				String[] misc = newMsgs[i].split(":", 2);
				
				if (misc[0].equals("InitGame")) {
					/**
					 * InitGame: \_Maps\Stock\_Mod\Before The Dawn\_ModUpdate\May 2008\_ModVer\Alpha 002\fs_game\mods/btd\g_compassShowEnemies\0\g_gametype\war\gamename\Call of Duty 4\mapname\mp_strike\protocol\6\shortversion\1.7\sv_allowAnonymous\0\sv_disableClientConsole\0\sv_floodprotect\4\sv_hostname\^3[GAS]^7Modweek: BTD Zombie - ^5The GoldenAK Community\sv_maxclients\20\sv_maxPing\250\sv_maxRate\25000\sv_minPing\0\sv_privateClients\0\sv_punkbuster...
					 */
					
					String[] info = misc[1].split("\\\\");
					String gamemode = "none";
					String map = "none";
					String mod = null;
					
					for (int p = 1; p < info.length; p++) {
						if (info[p].equals("g_gametype")) gamemode = info[p + 1];
						if (info[p].equals("mapname")) map = info[p + 1];
						if (info[p].equals("_Mod")) mod = info[p + 1];
					}
					
					fMsg.add(new DistributeMessage(echoChannel, formatMsg("\u000302Started map %s with gamemode %s", parseMapname(map), parseGametype(gamemode)) + (mod != null ? " and mod " + mod : "")));
				} else if (misc[0].equals("ShutdownGame")) {
					fMsg.add(new DistributeMessage(echoChannel, "\u000302End of the round"));
				} else if (misc[0].equals("ExitLevel")) {
					// TODO Some statistics when the map is over.
					fMsg.add(new DistributeMessage(echoChannel, "\u000302End of the map"));
					
					// Reset everything.
					for (int nId = 0; nId < players.length; nId ++) {
						players [nId] = null;
					}
				} else {
					// Unknown message. For the sake of completeness, we send this to IRC as well.
					fMsg.add(new DistributeMessage(echoChannel, newMsgs[i]));
				}
			}
		}
	}
	
	/**
	 * A line has been read from IRC. In here we check if it's a command we need to execute.
	 */
	public void onChannelPrivmsg (IrcBot pBot, String sChannel, String sNickname, String sMessage) {
		if (sMessage.charAt (0) != '!') {
			return;
		}
		
		// FIXME: HIGH PRIO! Levels for different commands. Need to work this out though.
		
		// Check if the command is triggered in the right channel
		if (Arrays.binarySearch(cmdChannel, sChannel.toLowerCase ()) < 0) {
			return;
		}
		
		// TODO commands: !msg, !players, !rcon
		if (sMessage.startsWith ("!msg")) {
			if (sMessage.length () < 6) {
				pBot.out.privmsg (sChannel, "Usage: !msg Message");
			} else {
				// Send a message to the server using rcon.
				rcon.sendCmd("say [" + sNickname + "] " + sMessage.substring (5));
			}
		} else if (sMessage.startsWith ("!players")) {
			if (sMessage.length () == 8) {
				// Give a list of players currently in the server.
				try {
					CoD4Query q = new CoD4Query (
						config.get ("rconhost"), 
						Integer.parseInt (config.get ("rconport")), 
						Integer.parseInt (config.get ("rcontimeout"))
					);
					Player [] p = q.getPlayers();
					HashMap settings = q.getSettings();
					String out = "" + (char)0x03 + "07" + (char)0x02 + "Players" + (char)0x0F + " (" + p.length + "/" + settings.get("sv_maxclients") + "): ";
					
					if (p.length == 0) {
						out += "" + (char)0x03 + "14No players online...";
					} else {
						for (int i = 0; i < p.length; i++) {
							// Strip colors because the logs don't include colors
							String strippedName = stripColor(p[i].name);
							
							if (playerID.containsKey(strippedName)) {
								PlayerStats curPlayer = players[playerID.get(strippedName)];
								out += parseColor(p[i].name) + " (03" + curPlayer.kills.size() + "/04" + curPlayer.deaths.size() + "), ";
							} else {
								// Player not known in the players array, so assume 0 kills and deaths.
								out += parseColor(p[i].name) + " (030/040), ";
							}
						}
						out = out.substring(0, out.length() - 2);
					}
					
					fMsg.add(new DistributeMessage(sChannel, out));
				} catch (Exception e) {
					fMsg.add(new DistributeMessage(sChannel, "Could not query server: " + e));
					//e.printStackTrace();
				}
			} else {
				// Search for players matching the params.
				try {
					CoD4Query q = new CoD4Query (
						config.get ("rconhost"),
						Integer.parseInt (config.get ("rconport")),
						Integer.parseInt (config.get ("rcontimeout"))
					);
					Player [] p = q.getPlayers();

					Vector<Player> match = new Vector<Player> ();
				
					// Get the name from the parameters given by the user
					String req = sMessage.substring (9);
					String reqLow = req.toLowerCase();
				
					// Loop through all players to find our man
					for (int i = 0; i < p.length; i++) {
						if (stripColor(p[i].name).toLowerCase().contains(reqLow)) {
							match.add(p[i]);
						}
					}
				
					if (match.size() == 1) {
						// One match, display everything we can
						// TODO: this is not all we can display, see the PlayerStats array (var: players)
						fMsg.add(new DistributeMessage(sChannel, "7Name: " + parseColor(match.get(0).name) + " - 7Score: " + match.get(0).score + " - 7Ping: " + match.get(0).ping));
					} else if (match.size() == 0) {
						// Nothing found
						fMsg.add(new DistributeMessage(sChannel, "No players found matching \"" + req + "\"."));
					} else {
						// We found a couple of people, show the names so they can be more specific
						String out = "";
						for (int i = 0; i < match.size(); i++) {
							out += parseColor(match.get(i).name) + ", ";
						}
						out = out.substring(0, out.length() - 2);
						fMsg.add(new DistributeMessage(sChannel, "" + match.size() + " players found matching \"" + req + "\": " + out));
					}
				} catch (Exception e) {
					fMsg.add(new DistributeMessage(sChannel, "Could not query server: " + e));
					//e.printStackTrace();
				}
			}
		}
		else if (sMessage.startsWith ("!info")) {
			try {
				CoD4Query pQuery = new CoD4Query (
					config.get ("rconhost"),
					Integer.parseInt (config.get ("rconport")),
					Integer.parseInt (config.get ("rcontimeout"))
				);
				HashMap <String, String> hmSettings = pQuery.getSettings();
				
				fMsg.add (new DistributeMessage (
					sChannel, parseColor (hmSettings.get ("sv_hostname")) +
					" - Map: " + parseMapname (hmSettings.get ("mapname")) +
					" - Gametype: " + parseGametype (hmSettings.get ("g_gametype")) +
					" - Ping: " + hmSettings.get ("ping") + "ms " +
					" - Players: " + hmSettings.get ("numclients") + 
					"/" + hmSettings.get ("sv_maxclients")
				));
			} catch (Exception pException) {
				fMsg.add (new DistributeMessage (sChannel, "Could not query server: " + pException));
			}
		}
		
		
		/* Disabled until a level system has been made.
		 else if (sMessage.equals ("!nextmap")) {
			// Run the next map.
			rcon.sendCmd ("map_rotate");
		} else if (sMessage.startsWith ("!rcon")) {
			if (sMessage.trim ().length () == 5) {
				// Usage
			}
			else {
				String cmd = sMessage.substring(6);
				rcon.sendCmd (cmd);
				fMsg.add (new DistributeMessage (echoChannel, "Rcon command sent: " + cmd));
			}
		}*/
		
		return;
	}
	
	/**
	 * An sprintf() like method which also replaces {NAME} with [ID] NAME.
	 */
	private String formatMsg (String format, Object ... args) {
		format = format.replace ("{NAME}", "\u000314[%02d]\u000F %s");
		return String.format (format, args);
	}
	
	/**
	 * Updates the array with players with the given information.
	 */
	private void updatePlayer (int nId, String sName, String sKeyHash) {
		if (nId == -1) {
			return;
		}
		
		if (players [nId] == null) {
			players [nId] = new PlayerStats ();
			players [nId].id = nId;
			players [nId].name = sName;
			players [nId].keyHash = sKeyHash;
			
			playerID.put (sName, nId);
		} else if (!players [nId].name.equals (sName)) {
			// Changed name.
			fMsg.add (new DistributeMessage (echoChannel, formatMsg("{NAME} changed his/her name to %s", nId, "\u000310" + players [nId].name, sName)));
			
			// Update player ID <-> player name map.
			playerID.remove (players [nId].name);
			playerID.put (sName, nId);
			
			// And finally update the cached name as well.
			players [nId].name = sName;
		}
	}
	
	/**
	 * Parses the bodypart from the logfile syntax to something nicer to read.
	 */
	public String parseBodyPart (String bodyPart) {
		String [] pieces = bodyPart.split ("_");
		String [] newPieces = new String [10];
		int c = pieces.length;
		if (c > 1 && (pieces [c - 1].equals ("upper") || pieces [c - 1].equals ("lower"))) {
			System.arraycopy (pieces, 0, newPieces, 1, c);
			newPieces [0] = pieces [c - 1];
			newPieces [c] = "";
		} else {
			newPieces = pieces;
		}
		
		String out = "";
		for (int i = 0; i < newPieces.length && newPieces [i] != null; i ++) {
			out += newPieces [i] + " ";
		}
		
		return out.trim ();
	}
	
	/**
	 * Converts the colorcodes from Call of Duty's format to mIRC's format.
	 */
	public static String parseColor(String message) {
		HashMap<Integer, Integer> colorMap = new HashMap<Integer, Integer>();
		{
			colorMap.put(0, 1);
			colorMap.put(1, 4);
			colorMap.put(2, 3);
			colorMap.put(3, 8);
			colorMap.put(4, 2);
			colorMap.put(5, 11);
			colorMap.put(6, 13);
			colorMap.put(7, 1);
			colorMap.put(8, 14);
			colorMap.put(9, 14);
		}
		
		int len = message.length();
		boolean inColor = false;
		String out = "";
		
		for (int i = 0; i < len; i++) {
			if (message.charAt(i) == '^' && len - 1 >= i + 1 && 
				Character.isDigit(message.charAt(i + 1))) {
				i++;
				// There's a colorcode.
				if (message.charAt(i) != '7') {
					// It's not white, place the right mIRC colorcode.
					if (!inColor) inColor = true;
					out += "\u0003" + colorMap.get(Integer.parseInt(Character.toString(message.charAt(i))));
				} else if (inColor) {
					// It's white, just reset the colors.
					out += "\u000F";
				}
			} else {
				// Nothing special, add the character.
				out += message.charAt(i);
			}
		}
		
		if (inColor) {
			// Reset the color if there's still one open.
			out += "\u000F";
		}
		
		return out;
	}
	
	/**
	 * Removes the Call of Duty colorcodes from the message.
	 */
	public static String stripColor(String message) {
		String out = "";
		int len = message.length();
		
		for (int i = 0; i < len; i++) {
			if (message.charAt(i) == '^' && len - 1 >= i + 1 && 
				Character.isDigit(message.charAt(i + 1))) {
				// This is a colorcode. Remove it.
				i = i + 2;
			}
			
			out += message.charAt(i);
		}
		
		return out;
	}
	
	/**
	 * Returns a neat name for this gametype, if it's known.
	 */
	public static String parseGametype(String gametype) {
		HashMap<String, String> gtMap = new HashMap<String, String>();
		{
			gtMap.put("dm", "Free-for-all");
			gtMap.put("koth", "Headquarters");
			gtMap.put("sab", "Sabotage");
			gtMap.put("war", "Team Deathmatch");
			gtMap.put("dom", "Domination");
			gtMap.put("sd", "Search & Destroy");
			gtMap.put("ctf", "Capture the Flag");
			gtMap.put("ctfb", "Capture the Flag Back");
			gtMap.put("htf", "Hold the Flag");
			gtMap.put("ftag", "Freeze Tag");
		}
		
		if (gtMap.containsKey(gametype)) {
			return gtMap.get(gametype);
		} else if (gametype.length() == 0) {
			return gtMap.get("war");
		} else {
			return gametype;
		}
	}
	
	/**
	 * Parses the internal mapname of Call of Duty 4 
	 */
	public static String parseMapname(String mapname) {
		mapname = mapname.replace("mp_", "");
		
		if (mapname.equals("citystreets")) {
			return "District";
		} else if (mapname.equals("cargoship")) {
			return "Wet Work";
		} else if (mapname.equals("farm")) {
			return "Downpour";
		} else if (mapname.equals("convoy")) {
			return "Ambush";
		} else if (mapname.equals("crash_snow")) {
			return "Winter Crash";
		} else if (mapname.equals("carentan")) {
			return "Chinatown";
		} else {
			return mapname.substring(0, 1).toUpperCase() + mapname.substring(1);
		}
	}
	
	/**
	 * Parses the internal names of CoD4 weapon to something everybody
	 * understands.
	 */
	public String parseWeapon(String weapon) {
		// TODO: delete this and put this in a configfile, to allow user config
		if (weapon.substring(weapon.length() - 3).equals("_mp")) {
			weapon = weapon.substring(0, weapon.length() - 3);
		}
		
		String out = "";
		String[] pieces = weapon.split("_");
		if (Arrays.binarySearch(new String[] {"artillery", "claymore", "concussion", "deserteagle", "deserteaglegold", "destructible", "frag"}, pieces[0]) < 0) {
			if (Arrays.binarySearch(new String[] {"barrett", "beretta", "dragunov", "remington700", "skorpion", "uzi", "winchester1200"}, pieces[0]) < 0) {
				pieces[0] = pieces[0].toUpperCase();
			}
			
			if (pieces.length > 1 && pieces[1].equals("bipod")) {
				out = "Mounted " + pieces[0];
			} else if (pieces.length > 1) {
				out = pieces[0] + " " + (pieces[0].equals("GL") ? pieces[1].toUpperCase() : pieces[1]);
			} else {
				out = pieces[0];
			}
			out = out.replace("gl", "grenade launcher");
			out = out.replace("GL", "grenade launcher");
			out = out.replace("acog", "ACOG");
			out = out.replace("reflex", "red dot sight");
			out = out.replace("saw", "M249 SAW");
		} else if (pieces[0].length() >= 11 && pieces[0].substring(0, 11).equals("deserteagle")) {
			out = "Desert Eagle";
			if (pieces[0].length() > 11) {
				out = "Golden " + out;
			}
		} else if (pieces[0].equals("frag")) {
			if (pieces.length > 2 && pieces[2].equals("short")) {
				out = "martyrdom";
			} else {
				out = "frag grenade";
			}
		} else if (pieces[0].equals("concussion")) {
			out = "stun grenade";
		} else {
			for (int i = 0; i < pieces.length; i++) {
				out += pieces[i] + " ";
			}
			out = out.substring(0, out.length() - 1);
		}
		out = out.substring(0, 1).toUpperCase() + out.substring(1);

		return out;
	}
	
	/**
	 * Returns the messages which need to be distributed over the bots in an
	 * array.
	 */
	public DistributeMessage[] getDistributeMessages() {
		if (fMsg.size() == 0) {
			return null;
		}
		
		DistributeMessage[] aMessages = new DistributeMessage [fMsg.size ()];
		aMessages = fMsg.toArray (aMessages);
		
		fMsg.clear ();
		
		return aMessages;
	}
	
	/**
	 * This class holds all information over a single player in the server.
	 */
	class PlayerStats {
		public int id;
		public String name;
		public String keyHash;
		
		/**
		 * True = allies, false = axis. Not used at the moment.
		 */
		public boolean team;
		
		public Vector<PlayerKill> kills = new Vector<PlayerKill>();
		public int currentKillstreak  = 0;
		public int biggestKillstreak  = 0;
		
		public Vector<PlayerKill> deaths = new Vector<PlayerKill>();
		public int currentDeathstreak = 0;
		public int biggestDeathstreak = 0;
	}
	
	/**
	 * A single kill or death.
	 */
	class PlayerKill {
		/**
		 * ID of the person who this object applies to.
		 */
		public int id;
		
		public String weapon = "none";
		public String bodyPart = "none";
		public String type = "";
		
		public PlayerKill(int _id, String _weapon, String _bodyPart, String _type) {
			id = _id;
			weapon = _weapon;
			bodyPart = _bodyPart;
			type = _type;
		}
	}
	
	/**
	 * This class handles querying a Call of Duty 4 server. Might work with CoD2 and CoD5 as well.
	 */
	public class CoD4Query {
		/**
		 * The address to query.
		 */
		private InetAddress m_pAddress;
		
		/**
		 * The port on which the Call of Duty server runs.
		 */
		private int m_nPort;
		
		/**
		 * The time in milliseconds to wait for a reply.
		 */
		private int m_nTimeout;
		
		/**
		 * The parsed settings of the queried CoD server.
		 */
		private HashMap<String, String> m_hmSettings = new HashMap<String, String> ();
		
		/**
		 * The players in the server.
		 */
		private Player [] m_aPlayers;
		
		/**
		 * This constructor takes a string as hostname.
		 */
		public CoD4Query (String sHostname, int nPort, int nTimeout) throws Exception {
			m_pAddress = InetAddress.getByName(sHostname);
			m_nPort    = nPort;
			m_nTimeout = nTimeout;
			
			doQuery();
		}
		
		/**
		 * Query the server and store the settings and the players currently 
		 * ingame.
		 */
		public void doQuery () throws Exception {
			byte [] aBuffer = new byte [4096];
			aBuffer [0] = aBuffer [1] = aBuffer [2] = aBuffer [3] = -1;
			
			// I went on the internet, and I found this:
			String sRequest  = new String ("getstatus");
			byte [] aRequest = sRequest.getBytes();
	        System.arraycopy (aRequest, 0, aBuffer, 4, aRequest.length);
			
			DatagramSocket pSocket = new DatagramSocket ();
			DatagramPacket pSendPacket = new DatagramPacket (aBuffer, aBuffer.length, m_pAddress, m_nPort);
			DatagramPacket pReceivePacket = new DatagramPacket (aBuffer, aBuffer.length);
			
			// Give up after some time.
			pSocket.setSoTimeout (m_nTimeout);
			pSocket.send (pSendPacket);
			
			// Determine the ping...
			long lDuration = System.nanoTime();
			// ...while receiving.
			pSocket.receive (pReceivePacket);
			double dPing = (System.nanoTime () - lDuration) / 1000000;
			
			String [] aResult = new String (aBuffer).trim ().substring (4).split ("\n");
			if (aResult [0].equals ("statusResponse")) {
				// Okay we're doing great so far.
				String[] aOptions = aResult [1].split ("\\\\");
				
				for (int i = 1; i < aOptions.length; i += 2) {
					m_hmSettings.put (aOptions [i], aOptions [i + 1]);
				}
				
				m_hmSettings.put ("ping", String.valueOf (dPing));
				m_hmSettings.put ("numclients", String.valueOf (aResult.length - 2));
				
				m_aPlayers = new Player [aResult.length - 2];
				
				for (int i = 2; i < aResult.length; i++) {
					// score ping "name"
					String[] aSplit = aResult [i].split (" ", 3);
					
					Player pPlayer = new Player ();
					pPlayer.score  = Integer.parseInt (aSplit [0]);
					pPlayer.ping   = Integer.parseInt (aSplit [1]);
					pPlayer.name   = aSplit [2].substring (1, aSplit [2].length () - 1);
					
					m_aPlayers [i - 2] = pPlayer;
				}
			} else {
				// What the heck? Something went wrong!
				throw new Exception ("Could not query server, wrong response: " + aResult [0]);
			}
		}
		
		/**
		 * Returns the settings of the queried server.
		 */
		public HashMap<String, String> getSettings() {
			return m_hmSettings;
		}
		
		/**
		 * Returns the players of the queried server.
		 */
		public Player [] getPlayers() {
			return m_aPlayers;
		}
		
		/**
		 * Quick and easy class to store the information per player.
		 */
		class Player {
			/**
			 * The score of this player.
			 */
			public int score;
			
			/**
			 * Their ping.
			 */
			public int ping;
			
			/**
			 * And of course the name of this player.
			 */
			public String name;
		}
	}
	
	/**
	 * This class sends all rcon commands to the server and saves the responses,
	 * without flooding it.
	 */
	class Rcon {
		/**
		 * The server we're going to query.
		 */
		private InetAddress m_pAddress;
		
		/**
		 * The port the CoD server is running on.
		 */
		private int m_nPort;
		
		/**
		 * The rcon password. We'll need this.
		 */
		private String m_sPassword;
		
		/**
		 * The time in milliseconds to wait for an answer from the CoD server.
		 */
		private int m_nTimeout;
		
		/**
		 * The timestamp of the next time we can query the server again in 
		 * milliseconds. This is used to prevent flooding of the rcon and 
		 * therefore it blocking us.
		 */
		private long m_tsNextLoop = 0;
		
		/**
		 * Commands which need to be sent.
		 */
		private Vector<String> m_vCmd = new Vector<String> ();
		
		/**
		 * Commands which have been sent and have a result.
		 */
		private Vector<RconMsg> m_vResult = new Vector<RconMsg> ();
		
		/**
		 * Constructs an Rcon object for a specific CoD4 server.
		 */
		public Rcon (String sHostname, int nPort, String sPassword, int nTimeout) throws UnknownHostException {
			m_pAddress  = InetAddress.getByName(sHostname);
			m_nPort     = nPort;
			m_sPassword = sPassword;
			m_nTimeout  = nTimeout;
		}
		
		/**
		 * Queues a command.
		 */
		public void sendCmd (String sCmd) {
			m_vCmd.add (sCmd);
		}
		
		/**
		 * Return true if there are commands to be sent.
		 */
		public boolean hasCmd () {
			return (m_vCmd.size () > 0);
		}
		
		/**
		 * Gets the first new result (FIFO)
		 */
		public RconMsg getResult () {
			return m_vResult.remove (0);
		}
		
		/**
		 * Returns true if there are new results.
		 */
		public boolean hasResult () {
			return (m_vResult.size () > 0);
		}
		
		/**
		 * Sends all queued commands to the rcon and stores the results.
		 */
		public void loop () throws Exception {
			if (System.currentTimeMillis () < m_tsNextLoop) {
				return;
			}
			
			if (!hasCmd ()) {
				return;
			}
			
			byte [] aBuffer = new byte [4096];
			
			String sCmd     = m_vCmd.remove (0);
			String sRequest = "rcon \"" + m_sPassword + "\" " + sCmd;
			
			// The first four bytes.
			aBuffer [0] = aBuffer [1] = aBuffer [2] = aBuffer [3] = -1;
			
			byte [] aRequest = sRequest.getBytes();
			for (int i = 0; i < aRequest.length; i ++) {
				aBuffer [i + 4] = aRequest [i];
			}
			
			// Close it with a null byte.
			aBuffer [aRequest.length + 4] = (byte) 0;
			
			DatagramSocket pSocket = new DatagramSocket ();
			DatagramPacket pSendPacket = new DatagramPacket (aBuffer, aBuffer.length, m_pAddress, m_nPort);
			DatagramPacket pReceivePacket = new DatagramPacket (aBuffer, aBuffer.length);
			pSocket.setSoTimeout (m_nTimeout);
			
			// Send...
			pSocket.send (pSendPacket);
			// ...and receive.
			pSocket.receive (pReceivePacket);
			
			m_vResult.add (new RconMsg (sCmd, aBuffer));
			
			m_tsNextLoop = System.currentTimeMillis () + 1000;
		}
		
		/**
		 * A simple struct-like class which keeps the command and response from
		 * an rcon command apart.
		 */
		class RconMsg {
			/**
			 * The command to execute (read: send to the remote console).
			 */
			public String cmd = "";
			
			/**
			 * Together with the command, the result. If it's already been 
			 * executed, that is.
			 */
			public String res = "";
			
			/**
			 * The constructor removes the 4 bytes that prefix rcon messages.
			 */
			public RconMsg(String c, byte[] buf) {
				cmd = c;
				// Remove weird bytes in front
				res = new String(buf).trim().substring(4);
			}
		}
	}
}